﻿using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Audio;
using System.IO;
using Mario.Characters;
namespace Mario.Structures
{

    class Level : IDisposable
    {

        // Physical structure of the level.
        private Tile[,] tiles;
        private Layer[] layers;
        // The layer which entities are drawn on top of.
        private const int EntityLayer = 2;

        // Entities in the level.
        public Player Player
        {
            get { return player; }
        }
        Player player;

        private List<Coin> coins = new List<Coin>();
        private List<Enemy> enemies = new List<Enemy>();

        // Key locations in the level.        
        private Vector2 start;
        private Point exit = InvalidPosition;
        private static readonly Point InvalidPosition = new Point(-1, -1);

        // Level game state.
        private float cameraPosition;
        private Random random = new Random(354668); // Arbitrary, but constant seed

        public int Score
        {
            get { return score; }
        }
        int score;

        public bool ReachedExit
        {
            get { return reachedExit; }
        }
        bool reachedExit;

        public TimeSpan TimeRemaining
        {
            get { return timeRemaining; }
        }
        TimeSpan timeRemaining;

        private const int PointsPerSecond = 5;

        // Level content.        
        public ContentManager Content
        {
            get { return content; }
        }
        ContentManager content;

        private SoundEffect exitReachedSound;

        public Level(IServiceProvider serviceProvider, string path)
        {
            // Create a new content manager to load content used just by this level.
            content = new ContentManager(serviceProvider, "Content");

            timeRemaining = TimeSpan.FromMinutes(5.0);

            LoadTiles(path);

            // Load background layer textures. For now, all levels must
            // use the same backgrounds and only use the left-most part of them.
            layers = new Layer[3];
            layers[0] = new Layer(Content, "Backgrounds/Layer0", 0.2f);
            layers[1] = new Layer(Content, "Backgrounds/Layer1", 0.5f);
            layers[2] = new Layer(Content, "Backgrounds/Layer2", 0.8f);

            // Load sounds.
            exitReachedSound = Content.Load<SoundEffect>("Sounds/clear");
        }

        private void LoadTiles(string path)
        {
            // Load the level and ensure all of the lines are the same length.
            int width;
            List<string> lines = new List<string>();
            using (StreamReader reader = new StreamReader(path))
            {
                string line = reader.ReadLine();
                width = line.Length;
                while (line != null)
                {
                    lines.Add(line);
                    if (line.Length != width)
                        throw new Exception(String.Format("The length of line {0} is different from all preceeding lines.", lines.Count));
                    line = reader.ReadLine();
                }
            }

            // Allocate the tile grid.
            tiles = new Tile[width, lines.Count];

            // Loop over every tile position,
            for (int y = 0; y < Height; ++y)
            {
                for (int x = 0; x < Width; ++x)
                {
                    // to load each tile.
                    char tileType = lines[y][x];
                    tiles[x, y] = LoadTile(tileType, x, y);
                }
            }

            // Verify that the level has a beginning and an end.
            if (Player == null)
                throw new NotSupportedException("A level must have a starting point.");
            if (exit == InvalidPosition)
                throw new NotSupportedException("A level must have an exit.");

        }

        private Tile LoadTile(char tileType, int x, int y)
        {
            Vector2 tilePosition = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
            switch (tileType)
            {
                // Blank space
                case '.':
                    return new Tile(null, TileCollision.Passable, tilePosition, this, false);

                // Exit
                case 'X':
                    return LoadExitTile(x, y);

                // Gem
                case 'G':
                    return LoadCoinTile(x, y, false);

                // Floating platform
                case '-':
                    return LoadTile("Platform", TileCollision.Platform, tilePosition, true);

                // Various enemies
                case 'A':
                    return LoadEnemyTile(x, y, "Goomba");
                case 'B':
                    return LoadEnemyTile(x, y, "Goombi");
                //Fireball powerup
                case 'F':
                    return LoadTile("Powerups/fireball", TileCollision.Passable, Special.Flower, tilePosition, false);
                //Mushroom powerup
                case 'M':
                    return LoadTile("Powerups/mushroom0", TileCollision.Passable, Special.Mushroom, tilePosition, false);
                // Platform block
                case '~':
                    return LoadVarietyTile("BlockB", 2, TileCollision.Platform, tilePosition, true);
                case '$':
                    return LoadTile("Blocks/metal1", TileCollision.Impassable, tilePosition, false);
                
                //Breakable 
                case '%':
                    return LoadTile("Walla", TileCollision.Breakable, tilePosition, true);
                // Player 1 start point
                case '1':
                    return LoadStartTile(x, y);
                //Metal block
                case '^':
                    return LoadTile("Blocks/metal1", TileCollision.Impassable, tilePosition, true);
                //Invisible block
                case 'I':
                    return new Tile(null, TileCollision.Invisible, tilePosition, this, false);
                //Question block
                case '?':
                    return LoadTile("Blocks/question", TileCollision.Special, Special.Question, tilePosition, true);
                // Impassable block
                case '#':
                    return LoadVarietyTile("Grass", 4, TileCollision.Impassable, tilePosition, true);
                //Land block
                case '*':
                    return LoadVarietyTile("Land", 4, TileCollision.Impassable, tilePosition, true);

                // Unknown tile type character
                default:
                    throw new NotSupportedException(String.Format("Unsupported tile type character '{0}' at position {1}, {2}.", tileType, x, y));
            }
        }

        private Tile LoadTile(String name, TileCollision collision, Vector2 tilePosition, Boolean isVisible)
        {
            return new Tile(name, collision, tilePosition, this, isVisible);
        }
        private Tile LoadTile(String name, TileCollision collision, Special special, Vector2 tilePosition, Boolean isVisible)
        {
            return new Tile(name, collision, special, tilePosition, this, isVisible);
        }

        private Tile LoadVarietyTile(string baseName, int variationCount, TileCollision collision, Vector2 tilePosition, Boolean isVisible)
        {
            int index = random.Next(variationCount);
            return LoadTile(baseName + index, collision, tilePosition, isVisible);
        }

        private Tile LoadStartTile(int x, int y)
        {
            Vector2 tilePosition = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
            if (Player != null)
                throw new NotSupportedException("A level may only have one starting point.");

            start = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
            player = new Player(this, start);

            return new Tile(null, TileCollision.Passable, tilePosition, this, true);
        }

        private Tile LoadExitTile(int x, int y)
        {
            Vector2 tilePosition = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
            if (exit != InvalidPosition)
                throw new NotSupportedException("A level may only have one exit.");

            exit = GetBounds(x, y).Center;

            return LoadTile("castle", TileCollision.Passable, tilePosition, true);
        }

        private Tile LoadEnemyTile(int x, int y, string spriteSet)
        {
            Vector2 position = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
            Vector2 tilePosition = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
            enemies.Add(new Enemy(this, position, spriteSet));

            return new Tile(null, TileCollision.Passable, tilePosition, this, true);
        }

        private Tile LoadCoinTile(int x, int y, bool isPowerUp)
        {
            Point position = GetBounds(x, y).Center;
            Vector2 tilePosition = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
            coins.Add(new Coin(this, new Vector2(position.X, position.Y), isPowerUp));
            return new Tile(null, TileCollision.Passable, tilePosition, this, true);
        }

        public void Dispose()
        {
            Content.Unload();
        }


        public TileCollision GetCollision(int x, int y)
        {
            // Prevent escaping past the level ends.
            if (x < 0 || x >= Width)
                return TileCollision.Impassable;
            // Allow jumping past the level top and falling through the bottom.
            if (y < 0 || y >= Height)
                return TileCollision.Passable;

            return tiles[x, y].collision;
        }
        public Edges GetEdgeCollision(int x, int y)
        {
            return tiles[x, y].Edges;
        }
        public void SetCollision(int x, int y, TileCollision collision)
        {
            tiles[x, y].collision = collision;
        }
        public void OnTileHit(int x, int y)
        {
            if (tiles[x, y].collision == TileCollision.Breakable)
            {
                tiles[x, y].OnHit();
                tiles[x, y] = new Tile(tiles[x, y].TextureName, TileCollision.Passable,Special.Breaked, tiles[x, y].Position, this, true);
                
            }
            else if (tiles[x, y].collision == TileCollision.Special && !tiles[x, y].wasHit)
            {
                tiles[x, y].OnHit();
                if (tiles[x, y - 1].special == Special.Question)
                    score += 5;
                if (tiles[x, y - 1].special == Special.Mushroom || tiles[x, y - 1].special == Special.Flower)
                {
                    tiles[x, y - 1].IsVisible = true;
                    tiles[x, y - 1].PowerUpAppears();
                }
            }
        }
        public Rectangle GetBounds(int x, int y)
        {
            return new Rectangle(x * Tile.Width, y * Tile.Height, Tile.Width, Tile.Height);
        }

        public int Width
        {
            get { return tiles.GetLength(0); }
        }

        public int Height
        {
            get { return tiles.GetLength(1); }
        }


        public void Update(GameTime gameTime)
        {
            // Pause while the player is dead or time is expired.
            if (!Player.IsAlive || TimeRemaining == TimeSpan.Zero)
            {
                // Still want to perform physics on the player.
                Player.ApplyPhysics(gameTime);
            }
            else if (ReachedExit)
            {
                // Animate the time being converted into points.
                int seconds = (int)Math.Round(gameTime.ElapsedGameTime.TotalSeconds * 100.0f);
                seconds = Math.Min(seconds, (int)Math.Ceiling(TimeRemaining.TotalSeconds));
                timeRemaining -= TimeSpan.FromSeconds(seconds);
                score += seconds * PointsPerSecond;
            }
            else
            {
                timeRemaining -= gameTime.ElapsedGameTime;

                Player.Update(gameTime);

                UpdateGems(gameTime);

                // Falling off the bottom of the level kills the player.
                if (Player.BoundingRectangle.Top >= Height * Tile.Height)
                    OnPlayerKilled(null);

                UpdateEnemies(gameTime);
                UpdateTiles(gameTime);

                // The player has reached the exit if they are standing on the ground and
                // his bounding rectangle contains the center of the exit tile. They can only
                // exit when they have collected all of the gems.
                if (Player.IsAlive &&
                    Player.IsOnGround &&
                    Player.BoundingRectangle.Contains(exit))
                {
                    OnExitReached();
                }
            }

            // Clamp the time remaining at zero.
            if (timeRemaining < TimeSpan.Zero)
                timeRemaining = TimeSpan.Zero;
        }

        private void UpdateGems(GameTime gameTime)
        {
            for (int i = 0; i < coins.Count; ++i)
            {
                Coin coin = coins[i];

                coin.Update(gameTime);

                if (coin.BoundingCircle.Intersects(Player.BoundingRectangle))
                {
                    coins.RemoveAt(i--);
                    OnGemCollected(coin, Player);
                }
            }
        }
        private void UpdateTiles(GameTime gameTime)
        {
            for (int y = 0; y < Height; ++y)
            {
                for (int x = 0; x < Width; ++x)
                {
                    if (tiles[x, y].special == Special.Mushroom && tiles[x, y].IsVisible && player.BoundingRectangle.Intersects(tiles[x, y].BoundingRectangle))
                    {
                        Player.GrowUp();
                        tiles[x, y].IsVisible = false;
                        tiles[x, y].PowerUpCollect();
                    }
                    else if (tiles[x, y].special == Special.Flower && tiles[x, y].IsVisible && player.BoundingRectangle.Intersects(tiles[x, y].BoundingRectangle))
                    {
                        if (!Player.IsBaby)
                        {
                            Player.OnFlowerPowerUp();
                            tiles[x, y].IsVisible = false;
                            tiles[x, y].PowerUpCollect();
                        }
                    }
                    tiles[x, y].Update(gameTime);
                }
            }
        }
        private void UpdateEnemies(GameTime gameTime)
        {
            for (int i = 0; i < enemies.Count; i++)
            {
                enemies[i].Update(gameTime);

                // Touching an enemy instantly kills the player
                if (enemies[i].IsAlive && enemies[i].BoundingRectangle.Intersects(Player.BoundingRectangle))
                {
                    if (Player.IsPoweredUp)
                    {
                        OnEnemyKilled(enemies[i], Player);
                    }
                    else
                        if (Player.BoundingRectangle.Bottom < enemies[i].BoundingRectangle.Bottom - enemies[i].BoundingRectangle.Height / 2)
                        {
                            OnEnemyKilled(enemies[i], Player);
                        }
                        else
                        {
                            OnPlayerKilled(enemies[i]);
                        }
                }
                foreach (FireBall ball in Player.fireBalls)
                {
                    if (enemies[i].IsAlive && ball.BoundingCircle.Intersects(enemies[i].BoundingRectangle))
                    {
                        OnEnemyKilled(enemies[i], Player);
                        ball.Visible = false;
                    }
                }
            }
        }
        private void OnEnemyKilled(Enemy enemy, Player killedBy)
        {
            enemy.OnKilled(killedBy);
        }

        private void OnGemCollected(Coin coin, Player collectedBy)
        {
            score += coin.PointValue;

            coin.OnCollected(collectedBy);
        }

        private void OnPlayerKilled(Enemy killedBy)
        {
            Player.OnKilled(killedBy);
        }

        private void OnExitReached()
        {
            Player.OnReachedExit();
            exitReachedSound.Play();
            reachedExit = true;
        }
        public void StartNewLife()
        {
            Player.Reset(start);
        }

        public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            spriteBatch.Begin();
            for (int i = 0; i <= EntityLayer; ++i)
                layers[i].Draw(spriteBatch, cameraPosition);
            spriteBatch.End();
            ScrollCamera(spriteBatch.GraphicsDevice.Viewport);
            Matrix cameraTransform = Matrix.CreateTranslation(-cameraPosition, 0.0f, 0.0f);
            spriteBatch.Begin(SpriteSortMode.Immediate,
                  BlendState.AlphaBlend,
                  null, null, null, null,
                  cameraTransform);

            DrawTiles(gameTime, spriteBatch);

            foreach (Coin gem in coins)
                gem.Draw(gameTime, spriteBatch);

            Player.Draw(gameTime, spriteBatch);

            foreach (Enemy enemy in enemies)
                enemy.Draw(gameTime, spriteBatch);
            spriteBatch.End();
            spriteBatch.Begin();
            for (int i = EntityLayer + 1; i < layers.Length; ++i)
                layers[i].Draw(spriteBatch, cameraPosition);
            spriteBatch.End();

        }

        private void DrawTiles(GameTime gameTime, SpriteBatch spriteBatch)
        {
            // Calculate the visible range of tiles.
            int left = (int)Math.Floor(cameraPosition / Tile.Width);
            int right = left + spriteBatch.GraphicsDevice.Viewport.Width / Tile.Width;
            right = Math.Min(right, Width - 1);

            // For each tile position
            for (int y = 0; y < Height; ++y)
            {
                for (int x = left; x <= right; ++x)
                {
                    // If there is a visible tile in that position
                    Texture2D texture = tiles[x, y].texture;
                    if (texture != null && tiles[x, y].IsVisible)
                    {
                        // Draw it in screen space.
                        Vector2 position = new Vector2(x, y) * Tile.Size;
                        tiles[x, y].Draw(gameTime, spriteBatch, position);
                        //spriteBatch.Draw(texture, position, Color.White);
                    }
                }
            }
        }
        private void ScrollCamera(Viewport viewport)
        {
            const float ViewMargin = 0.35f;
            // Calculate the edges of the screen.
            float marginWidth = viewport.Width * ViewMargin;
            float marginLeft = cameraPosition + marginWidth;
            float marginRight = cameraPosition + viewport.Width - marginWidth;

            // Calculate how far to scroll when the player is near the edges of the screen.
            float cameraMovement = 0.0f;
            if (Player.Position.X < marginLeft)
                cameraMovement = Player.Position.X - marginLeft;
            else if (Player.Position.X > marginRight)
                cameraMovement = Player.Position.X - marginRight;

            // Update the camera position, but prevent scrolling off the ends of the level.
            float maxCameraPosition = Tile.Width * Width - viewport.Width;
            cameraPosition = MathHelper.Clamp(cameraPosition + cameraMovement, 0.0f, maxCameraPosition);
        }
    }
}
